import torch
import numpy as np

# 自动求梯度
# PyTorch提供的autograd包能够根据输⼊和前向传播过程⾃自动构建计算图，并执⾏行行反向传播。
# 本节将介绍如何使⽤用autograd包来进⾏行行⾃自动求梯度的有关操作。

# 创建tensor数据时，如果将其属性.requires_grad设置为True
# 它将开始追踪(track)在其上的所有操作（这样就可以利利⽤用链式法则进⾏行行梯度传播了了）
# 完成计算后，可以调用.backward()来完成所有梯度计算，此tensor的梯度将积累到.grad属性中

# 创建一个tensor，并设置requires_grad=true
x = torch.ones((2, 2), requires_grad=True)
print(x)
# .grad_fn判断该tensor是否由其他函数运算创建出来的，没有则为否，有则将相关的运算对象返回出来,
# x这种直接创建的称之为叶子节点，叶子节点对应的grad_fn为none
print(x.grad_fn)
y = x + 2
print(y)
print(y.grad_fn)

z = y * y * 3
# mean()求均值
out = z.mean()
print(z, out)

# 通过.requires_grad_()来用in-place的方式来改变requires_grad属性
a = torch.randn(2, 2) # 默认requires_grad=false
a = ((a*3)/(a-1))
print(a)
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
print(a)
b = (a*a).sum()
print(b)
print(b.grad_fn)

# grad梯度
# out是一个标量，所以调用backward()不需要指定 求导变量 out.backward()等价与out.backward(torch.tensor(1.))
out.backward()  # out = a.mean(), z = y * y * 3, y = x + 2
print(x.grad)

# grad 在反向传播的过程中是累加的，这意味着每运行一次反向传播，梯度都会累加之前的梯度，所以在反向传播前需要把梯度清零

# 再来一次反向传播，注意梯度是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

# 先将梯度清零，再来一次反向传播
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

# 中断梯度跟踪
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
    y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad)
print(y1, y1.requires_grad) # True
print(y2, y2.requires_grad) # False
# y2没有算入梯度计算中，事实上，由于y2的定义是被torch.no_grad():包裹的
# 所以与y2有关的梯度是不会回传的，只有与y1有关的梯度才会回传，即x^2对x的梯度。
print(y3, y3.requires_grad) # True

# 修改tensor类型的值（tensor.requires_grad=true）,且不会被autograd记录（即不影响运算的反向传播），则可以对tensor.data数据操作
x = torch.ones(1,requires_grad=True)
print(x.data) # 还是⼀一个tensor
print(x.data.requires_grad) # 但是已经是独⽴立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了了值，不不会记录在计算图，所以不不会影响梯度传播
y.backward()
print(x) # 更更改data的值也会影响tensor的值
print(x.grad)

